Skip to content

[Server] Wire up notifications for changing prompt, resource & tools lists#234

Draft
chr-hertel wants to merge 1 commit intomainfrom
changed-events
Draft

[Server] Wire up notifications for changing prompt, resource & tools lists#234
chr-hertel wants to merge 1 commit intomainfrom
changed-events

Conversation

@chr-hertel
Copy link
Member

@chr-hertel chr-hertel commented Feb 1, 2026

This is a working draft for emitting change notifications on dynamically added prompts, resources and tools.

The main issue here is that we need that indirection between Registry, source of truth on changing lists, and Protocol, that is able so send notifications. That's why we have those events, but we never really adopted them.

I introduced a slim EventDispatcher/ListenerProvider setup, but it feels a bit flaky - see wire up in example. The service landscape within the SDK just grows in complexity.

TBD

  1. Event Dispatcher
    The issue I had was mostly about PSR-14 - we want to empower library users to inject their own event dispatcher, to easily register subscriber/listener/whatever - but with the current implementation we also want to add our own listeners for triggering the notifications (again, we need a decoupling between registry and protocol currently).
    With PSR-14 the EventDispatcherInterface used in the Registry doesn't offer us to register our own listeners after a user injected it via Builder::setEventDispatcher()

  2. Session ID State
    I need to call the Protocol::sendNotification(...) from the listeners, but didn't have the session at hand - and there is basically no state in the Protocol - which is great, but we don't have a service concept to retrieve the session - it is only state in the transport or arguments looped through half the SDK.
    I went for introducing the session as state in the Protocol - needs better null-checks tho, but wanted to check with @CodeWithKyrian first - the other option would be to keep the transport as state, since it also has the session already as state.

WDYT?

edit: this currently messes up the session handling within a fiber intermediate client request.

@chr-hertel chr-hertel added the improves spec compliance Improves consistency with other SDKs such as TyepScript label Feb 1, 2026
@chr-hertel chr-hertel added the needs more work Not ready to be merged yet, needs additional follow-up from the author(s). label Feb 8, 2026
@soyuka
Copy link
Contributor

soyuka commented Mar 19, 2026

I'm working on an MCP server that dynamically registers tools at runtime (tools are discovered progressively as the user navigates an API), so I'm hitting exactly the two problems you describe. Wanted to share what I ended up doing.

Notification delivery via Fiber::suspend()

I don't use the event system — I use Fiber::suspend() directly from the tool handler after a batch of registrations:

// After registering N tools in a loop:
if ($newToolsRegistered) {
    \Fiber::suspend([
        'type' => 'notification',
        'notification' => new ToolListChangedNotification(),
    ]);
}

This already works with the current SDK and has the advantage of batching — one notification after registering all discovered tools, rather than one event per registerTool() call.

Worth considering: the event-based approach and the Fiber::suspend approach need to coexist cleanly. If registerTool() synchronously dispatches an event that calls Protocol::sendNotification() while we're inside a Fiber handler, there could be reentrancy issues with the session state on Protocol.

Session state (your TBD #2)

This was the hardest part for me too. I needed session access outside of the handler to persist tool state across HTTP requests.

What I ended up doing:

  • For write paths (during tool handling): the session comes from the handler context. API Platform now propagates it via $context['mcp_session']: api-platform/core@86b97d5
  • For read paths (e.g. restoring tools on a subsequent request): I resolve the session from SessionStoreInterface using the Mcp-Session-Id header, with an in-memory cache fallback for stdio.

Storing session on Protocol (your current approach) makes sense as the central place, but I'd flag the Fiber concern — if $this->session gets set to null after save() in processInput(), but a Fiber is still suspended and later resumed, the session reference may be gone.

Thoughts on the PR

  1. listChanged capabilities always true — I'd keep these conditional. A static server that never calls registerTool(..., isManual: true) shouldn't advertise toolsListChanged. Clients may behave differently (polling, etc.) based on this capability.

  2. Per-registration events vs. batch — A single tool call in my server can discover 5-10 new tools at once. Getting one ToolListChangedEvent per registerTool() call would be noisy. Maybe a Registry::flush() / deferred dispatch pattern, or let consumers opt into batching?

  3. PSR-14 complexity — I share your concern about the growing service landscape. The ListenerProvider + Dispatcher + ChangeListener + Builder wiring is a lot of ceremony for what is essentially "notify the client when the tool list changes." The Fiber::suspend path already handles the in-handler case with zero new abstractions.

Related: #251 — I also ran into clear() wiping dynamically registered tools and needed to track discovered state on references.

Note: LLM generated comment based on my work I hope you're fine with it I thought it'd be nice to share my solutions :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improves spec compliance Improves consistency with other SDKs such as TyepScript needs more work Not ready to be merged yet, needs additional follow-up from the author(s).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants